Redux middleware


Posted by Christy on 2022-01-02

本文為 Lidemy [FE303] React 的好夥伴:Redux > Redux 核心:Middleware 的筆記內容

零、redux 的最後一塊拼圖: middleware

懶人工具包:Redux Toolkit

仲介:middleware

  • 函式處理仲介:redux thunk 讓 action 變身成函式,不只是物件

一、Redux Toolkit 簡稱 RTK

1. 為什麼要用它?

因 redux 基本設置(boilerplate)過多,寫起來很煩,因此 Redux Toolkit 就是把 Redux 包裝好,讓你不用自己去處理 store 那些的一個工具包,有點懶人包的概念。

2. 要怎麼安裝?

a. 結論:跟官網不同的是,我用的是 $ npx create-react-app@latest blog --template redux,這個會一次把 react/redux/redux toolkit/react-redux 都裝好

b. 安裝過程

b.1 指令 $ npx create-react-app my-app --template redux 一直報錯,就把 vs code 刪掉重裝了。

b.2 重裝沒效,最後用了 $ npx create-react-app@latest blog 雖然跑得起來,可是那是純 react,所以現在要跑的話要用下面的:

b.3 $ npx create-react-app@latest blog --template redux 就可以一次把 react/redux/redux toolkit/react-redux 都裝好

c. 檔案導覽

  • 裡面的 store、Provider 都幫你設置好了

  • createSlice 就像一個完整的功能: 裡面結合了 action, reducer, actionTypes

    • toolkit 底層用了 immer 這個套件能讓我們直接更改狀態,不用再寫什麼 ...state 回傳一個新的狀態之類的
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
  • toolkit 把 aciton 跟 actionTypes 結合在一起,變成一個函式

參考 Reducers and actions 下面的 methods createReducer()

二、Redux middleware

又稱中間件,是一個仲介的概念;看起來常用在「非同步的事件處理上」,例如:呼叫 API 或者處理登入事件等

我的理解:middleware 很像是交通仲介,例如說我有一個 action 是從 A 地到 B 地,那 middleware 幫我做的事可能是「買車票」

參考資料:Thunks and Async Logic詳解 Redux Middleware

三、Redux thunk 把物件般的 action 變成函式

Redux thunk 是一個執行函式的 middleware,把 action 變成函式,並且執行這個函式。

四、把 blog 加上 redux

?1. 整理資料夾結構

  • components folder

    • App folder

    • NavBar folder

  • pages

    • 各式各樣的頁面們.js
  • redux

    • reducers folder

      • postReducer.js
    • store.js

  • contexts.js

  • index.js

  • utils.js

  • WebAPI.js

註:解決的問題包含沒有裝 react-router-dom 以及檔案路徑問題(用越多工具就要裝來裝去,難怪需要工具管理程式來管理)

2. 02'30" 改寫單一文章功能

a. 實作步驟:

a.1 創建 store

// store.js

import { configureStore } from '@reduxjs/toolkit';
import postReducer from './reducers/postReducer';

export default configureStore({
    reducer: {
    posts: postReducer,
  },
});

a.2 創建 reducers 資料夾 > postReducer.js

// postReducer.js

import { createSlice } from '@reduxjs/toolkit';
import { getArticle } from '../../WebAPI';

// 定義 reducer 裡面的行為
export const postReducer = createSlice({
  name: 'posts',
  initialState: {
    isLoadingPost: false,
    // 這裡設為 null 會報錯:Uncaught TypeError: Cannot read properties of null
    // 正確寫法:post: "",
    post: null,
  },

  reducers: {
    setIsLoadingPost: (state, action) => {
      state.isLoadingPost = action.payload;
    },

    setPost: (state, action) => {
      state.post = action.payload;
    },
  }
});

export const { setIsLoadingPost, setPost } = postReducer.actions;

// call API
export const getPost = (id) => (dispatch) => {
  dispatch(setIsLoadingPost(true));
  getArticle(id)
    .then((res) => {
      dispatch(setPost(res));
      dispatch(setIsLoadingPost(false));
    })
    .catch((err) => {
      console.log(err);
    });
};

export default postReducer.reducer;

a.3 把剛剛在 reducer 定義的呼叫 API 的函式拿到 Article 頁面裡用

// ArticlePage.js

import { getPost } from '../../redux/reducers/postReducer';
import { useDispatch, useSelector } from "react-redux";

export default function ArticlePage() {
  let { id } = useParams();
  const dispatch = useDispatch();
  const isLoading = useSelector((store) => store.posts.isLoadingPost);
  const article = useSelector((store) => store.posts.post);

  useEffect(() => {
    dispatch(getPost(id));  
  }, [id, dispatch])

  return <Article article={article} />;
}

註:要測試 react and redux 有沒有串起來,影片裡面用的是 console.log(isLoading),因為預設值是 false,所以如果印出的值把它改成 true 還是正常印出來,那就表示有抓到

b. error log

debug 第一個路徑就是開 redux 的 devtool,去看看有沒有發生你預期的行為,在這裡就是有沒有發送 API 去抓資料

ArticlePage.js:29 Uncaught TypeError: Cannot read properties of null (reading 'title')

都改寫好以後,出現這個錯誤訊息,發現 Article component 沒有抓到標題跟內容,照著影片嘗試了 console.log(isLoading) 發現有從 false 變成 true,因此確定有把 react redux 串起來,API 的部分也沒有錯

註:console.log(isLoading) 超可怕的,整個 devtool 就一直跳訊息,電腦燒起來了

  • 嘗試:更改 props 名稱,但是發現不管什麼名稱都一樣,因為排除這個原因

  • 嘗試:把 useEffect 拿掉,發現有時候可以運作有時不行,怪怪的

  • 嘗試:把 cookie 跟紀錄全部清掉,但不確定有沒有影響

  • 嘗試:後來發現是「不可以重新整理頁面,如果重新整理頁面,就會發現壞了,而這個應該跟我的頁面是 SPA 有關」,上述錯誤訊息還是存在,但是只要「不重新整理頁面」功能就是好的。

  • 嘗試:reducer post 初始化狀態設為 "null" 是錯的,設為 "" 就不會有這個錯了,這樣重新整理頁面、怎麼點擊跳轉都是正常的,太好了。

註:後來發覺用 []""

五、改寫發表文章功能

a. 實作步驟

新增一個 reducer 裡面的狀態 -> newPost 去抓 API -> 引入 dispatch & selector -> 當新增文章時,去呼叫 API -> 當成功新增文章時,就導到首頁

要解決的問題:新增文章跳轉到首頁以後,再按一次新增文章,會跳到上一篇新增文章單一頁面,但應該是新增文章輸入頁面才對

第一種:離開頁面時,把 newpostresponse 清空

useEffect(() => {
  return () => {
    dispatch(setNewPostResponse(null));
  }
}, [dispatch])

第二種:

利用 redux thunk 能夠回傳一個 promise 的功能,直接拿到 newPostResponse 的值,連 selector 都省了

a. 原本在 reducer 抓 API 長這樣

export const newPost = (data) => (dispatch) => {
  newArticle(data).then((res) => {
    dispatch(setNewPostResponse(res));
  });
};

b. 但現在在裡面回傳一個 promise

export const newPost = (data) => (dispatch) => {
  return newArticle(data).then((res) => {
    dispatch(setNewPostResponse(res));
    return res;
  });
};

c. 接著在頁面那邊把那個 promise 改寫

const handleNewArticle = () => {
  setErrorMessage(null);
  if (!title || !body) {
    const errorMessage = "Please input missing field";
    setErrorMessage(errorMessage);
  } else {
    dispatch(newPost({
      title,
      body
    })).then((newPostResponse) => {
      if (newPostResponse && newPostResponse.id) {
        navigate("/posts/" + newPostResponse.id);
      }
    })
  }
};

第三種:

利用 isLoadingNewPost 來判斷時間點,新增一個 isLoadingNewPost 的狀態,藉由判斷狀態來規範何時要導回「剛剛新增的文章頁面」

有一個情況就是「剛剛正在 loading,而現在沒有在 loading 了」

if (!isLoadingNewPost && prevIsLoadingNewPost) -> 假設符合這個狀態,就表示已經拿到 response 了

作法:在 reducer 新增一個 isLoadingNewPost 的狀態,並且使用 usePrevious 這個別人做好的 hook,來實現這個做法

b. error log: VM4939:1 Uncaught (in promise) SyntaxError: Unexpected token < in JSON at position 0

問題描述:無法新增文章,不會跳轉頁面;打開 redux devtool,沒有任何紀錄,react redux 沒有串起來

b.1 解法一:Unexpected token < in JSON at position 0

  • API 網址錯誤

  • 把最後面的 .then((res) => res.json()); 換成 .then((res) => res.text());

這樣 redux dev tool 真的有去呼叫 newPost 那支 API 了,但是這個解法好奇怪

b.2 解法二:搞了半天是 WebAPI 裡面的寫法,需要的資料要解構語法,寫完以後總算有去呼叫 newPost 這個 API 了

然後再把 newArticlePage 裡面的名稱改好,要叫做 title & body 相關,不能用其他的

// 原本是寫成...(title, body)...

export const newArticle = ({ title, body }) => {
  const token = getAuthToken();
  return fetch(`${BASE_URL}/posts`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      authorization: `Bearer ${token}`,
    },
    body: JSON.stringify({
      title,
      body,
    }),
  }).then((res) => res.json());
};

六、簡單介紹 redux saga 與 redux observable

學習曲線很高,這兩個可能都要學超過一個月(?)其中要學 redux observable,建議先從 observable 開始,接著再進入 redux observable 比較好。










Related Posts

17. Iterator

17. Iterator

1211. Queries Quality and Percentage

1211. Queries Quality and Percentage

【隨堂筆記】C 語言基礎概念

【隨堂筆記】C 語言基礎概念


Comments